package org.finalministry.style;

import java.io.Serializable;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.security.auth.Subject;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.builder.ToStringStyle;

/**
 * Custom-specific implementations of apache commons' ToStringStyle String
 * builders. <br>
 * <br>
 * <b>Example toString() method</b><br>
 * <code>public String toString() {<br>
 *    return ToStringBuilder.reflectionToString(this,<br>
 *             CustomToStringStyle.Custom_STANDARD_STYLE);<br>
 * }</code><br>
 * 
 */
public class CustomStringStyle implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * Standard string style, based on ToStringStyle.SHORT_PREFIX_STYLE, but
	 * with extra formatters for Dates, Calendars, HBBase objects and byte[]
	 */
	public static final ToStringStyle CUSTOM_STANDARD_STYLE = new CustomStandardToStringStyle();

	// eg "2010:12:25 23:52 CET"
	private static final String DATE_FMT = "yyyy.MM.dd HH:mm:ss z";

	public CustomStringStyle() {

		super();
	}

	/**
	 * this class is private, callers must access the constant above
	 */
	private static final class CustomStandardToStringStyle extends ToStringStyle {

		private static final long serialVersionUID = 1L;

		/**
		 * Use the static constant instead of instantiating
		 */
		CustomStandardToStringStyle() {

			super();

			/*
			 * This style mimics SHORT_PREFIX_STYLE, so according to the commons
			 * sourcecode for SHORT_PREFIX_STYLE, we set the following two vars:
			 */
			this.setUseShortClassName(true);
			this.setUseIdentityHashCode(false);

		}

		/*
		 * append detail for _NON-NATIVE_ objects (non-Javadoc)
		 * 
		 * @see
		 * org.apache.commons.lang.builder.ToStringStyle#appendDetail(java.lang
		 * .StringBuffer, java.lang.String, java.lang.Object)
		 */
		protected void appendDetail(StringBuffer buf, String fieldName, Object obj) {

			String format = formatObjectValueIfSupported(obj);
			if (format != null) {
				buf.append(format);
			} else {
				super.appendDetail(buf, fieldName, obj);
			}

		}

		/*
		 * Append details for native byte[] arrays, as they tend to contain lots
		 * of data (non-Javadoc)
		 * 
		 * @see
		 * org.apache.commons.lang.builder.ToStringStyle#appendDetail(java.lang
		 * .StringBuffer, java.lang.String, byte[])
		 */
		protected void appendDetail(java.lang.StringBuffer buf, java.lang.String fieldName, byte[] byteArr) {

			buf.append(formatObjectValue(byteArr));
		}

		/*
		 * When an object has a field which is a collection, just show the short
		 * classname and size
		 * 
		 * @see
		 * org.apache.commons.lang.builder.ToStringStyle#appendDetail(java.lang
		 * .StringBuffer, java.lang.String, java.util.Collection)
		 */
		protected void appendDetail(StringBuffer buffer, String fieldName, Collection coll) {

			buffer.append(formatObjectValue(coll));
		}

		/*
		 * When and object has a field which is a Map, just show the keys (If
		 * the key is a string, then "value", if not, then short classname
		 * 
		 * @see
		 * org.apache.commons.lang.builder.ToStringStyle#appendDetail(java.lang
		 * .StringBuffer, java.lang.String, java.util.Map)
		 */
		protected void appendDetail(StringBuffer buffer, String fieldName, Map map) {

			buffer.append(formatObjectValue(map));

		}

		/*
		 * Similar to the appendDetail for a Collection field, just show the
		 * size
		 * 
		 * @see
		 * org.apache.commons.lang.builder.ToStringStyle#appendDetail(java.lang
		 * .StringBuffer, java.lang.String, java.lang.Object[])
		 */
		protected void appendDetail(StringBuffer buffer, String fieldName, Object[] array) {

			buffer.append(formatObjectValue(array));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.apache.commons.lang.builder.ToStringStyle#append(java.lang.
		 * StringBuffer, java.lang.String, java.lang.Object, java.lang.Boolean)
		 */
		public void append(StringBuffer buffer, String fieldName, Object value, Boolean fullDetail) {

			super.append(buffer, fieldName, value, fullDetail);

		}

		/**
		 * ensure singleton after serialization
		 */
		private Object readResolve() {

			return CustomStringStyle.CUSTOM_STANDARD_STYLE;
		}

	}

	/**
	 * Generate a better toString() for certain types of objects, using the
	 * object's toString() if not supported
	 * 
	 * @param obj
	 *            The object
	 * @return null if the object was null, the formatted toString otherwise
	 */
	public static String customToString(Object obj) {

		if (obj == null) {
			return null;
		}
		String res = formatObjectValueIfSupported(obj);

		// if we don't have a special formatter, just call toString()
		return res != null ? res : obj.toString();
	}

	/**
	 * For the given byte array, generate a string similar to "byte[size=300]"
	 * 
	 * @param byteArr
	 *            The byte array
	 * @return null if the object was null, the formatted toString otherwise
	 */
	private static String formatObjectValue(byte[] byteArr) {

		if (byteArr == null) {
			return null;
		}

		StringBuffer buf = new StringBuffer();
		buf.append("byte[size=");
		buf.append(byteArr.length);
		buf.append("]");
		return buf.toString();

	}

	/**
	 * For the given collection, generate a string similar to
	 * "ClassName{size=300}"
	 * 
	 * @param coll
	 *            The collection
	 * @return null if the object was null, the formatted toString otherwise
	 */
	private static String formatObjectValue(Collection<?> coll) {

		if (coll == null) {
			return null;
		}

		StringBuffer buf = new StringBuffer();
		buf.append(ClassUtils.getShortClassName(coll.getClass()));
		buf.append("{size=");
		buf.append(coll.size());
		buf.append("}");
		return buf.toString();

	}

	/**
	 * For the given map, generate a string similar to
	 * "ClassName{keys:"blah","moo","bing",ClassNameIfKeyIsNotString}"
	 * 
	 * @param map
	 *            The map
	 * @return null if the object was null, the formatted toString otherwise
	 */
	private static String formatObjectValue(Map<?, ?> map) {

		if (map == null) {
			return null;
		}

		StringBuffer buf = new StringBuffer();
		buf.append(ClassUtils.getShortClassName(map.getClass()));
		buf.append("{keys:");
		Iterator<?> it = map.keySet().iterator();
		while (it.hasNext()) {
			Object key = it.next();
			if (key instanceof String) {
				buf.append("\"");
				buf.append(key);
				buf.append("\"");
			} else {
				buf.append(ClassUtils.getShortClassName(key.getClass()));
			}
			if (it.hasNext()) {
				buf.append(",");
			}
		}
		buf.append("}");
		return buf.toString();
	}

	/**
	 * For the given Object array, generate a string similar to
	 * "ClassName[size=300]"
	 * 
	 * @param array
	 *            The object array
	 * @return null if the object array was null, the formatted toString
	 *         otherwise
	 */
	private static String formatObjectValue(Object[] array) {

		if (array == null) {
			return null;
		}

		StringBuffer buf = new StringBuffer();
		buf.append(ClassUtils.getShortClassName(array.getClass()));
		buf.append("[size=");
		buf.append(array.length);
		buf.append("]");
		return buf.toString();
	}

	/**
	 * This method either formats
	 * 
	 * @param obj
	 * @return
	 */
	private static String formatObjectValueIfSupported(Object obj) {

		StringBuffer buf = new StringBuffer();

		if (obj instanceof Date) {
			obj = new SimpleDateFormat(DATE_FMT).format(obj);
			buf.append(obj);
		} else if (obj instanceof Calendar) {
			obj = new SimpleDateFormat(DATE_FMT).format(((Calendar) obj).getTime());
			buf.append(obj);
		} else if (obj instanceof Subject) {
			// Subject has an irritating toString()
			Subject subject = (Subject) obj;
			buf.append(formatSubject(subject));
		} else {
			// object type isn't handled here, return null so caller knows it's
			// not just an empty answer
			return null;
		}

		return buf.toString();
	}

	/*
	 * Foramts the subject with counts of the principle classes
	 * "Subject{3*PrincipleClassNameA,5*PrincipleClassNameB}
	 */
	private static String formatSubject(Subject subject) {

		StringBuffer buf = new StringBuffer();
		buf.append("Subject{");

		// count principals by className
		// (Java1.5 syntax would make this so much cleaner/simpler)
		HashMap countsByClass = new HashMap();
		Iterator it = subject.getPrincipals().iterator();
		while (it.hasNext()) {
			Principal cur = (Principal) it.next();
			String className = ClassUtils.getShortClassName(cur.getClass());

			Integer classTotal = (Integer) countsByClass.get(className);
			if (classTotal == null) {
				classTotal = new Integer(0);
			}
			// Can't wait till we can use autoboxing
			countsByClass.put(className, new Integer(classTotal.intValue() + 1));
		}

		// collated, now pretty format "3*ClassNameA,5*ClassNameB..."
		it = countsByClass.keySet().iterator();
		while (it.hasNext()) {
			String className = (String) it.next();
			buf.append(countsByClass.get(className));// implicit
														// Integer.toString()
			buf.append("*");
			buf.append(className);
			if (it.hasNext()) {
				buf.append(",");
			}
		}

		buf.append("}");

		return buf.toString();
	}

}
